package com.hello.sandbox.core.system.location;

import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.AtomicFile;
import android.util.SparseArray;
import black.android.location.BRILocationListener;
import black.android.location.BRILocationListenerStub;
import com.hello.sandbox.SandBoxCore;
import com.hello.sandbox.core.env.BEnvironment;
import com.hello.sandbox.core.system.ISystemService;
import com.hello.sandbox.entity.location.BCell;
import com.hello.sandbox.entity.location.BLocation;
import com.hello.sandbox.entity.location.BLocationConfig;
import com.hello.sandbox.fake.frameworks.BLocationManager;
import com.hello.sandbox.utils.CloseUtils;
import com.hello.sandbox.utils.FileUtils;
import com.hello.sandbox.utils.Slog;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Fake location plan1: only GPS invocation is valid and other methods like addressed by cells are
 * intercepted at all. plan2: mock fake neighboring cells from LBS database and modify the result of
 * GPS invocation. plan3: cheat internal application at being given permission to access location
 * information but get data from BB. the final testing condition requires UI demo. Created by
 * BlackBoxing on 3/8/22.
 */
public class BLocationManagerService extends IBLocationManagerService.Stub
    implements ISystemService {
  public static final String TAG = "BLocationManagerService";

  private static final BLocationManagerService sService = new BLocationManagerService();
  private final SparseArray<HashMap<String, BLocationConfig>> mLocationConfigs =
      new SparseArray<>();
  private final BLocationConfig mGlobalConfig = new BLocationConfig();
  private final Map<IBinder, LocationRecord> mLocationListeners = new HashMap<>();
  private final Executor mThreadPool = Executors.newCachedThreadPool();

  public static BLocationManagerService get() {
    return sService;
  }

  private BLocationConfig getOrCreateConfig(int userId, String pkg) {
    synchronized (mLocationConfigs) {
      HashMap<String, BLocationConfig> pkgs = mLocationConfigs.get(userId);
      if (pkgs == null) {
        pkgs = new HashMap<>();
        mLocationConfigs.put(userId, pkgs);
      }
      BLocationConfig config = pkgs.get(pkg);
      if (config == null) {
        config = new BLocationConfig();
        config.pattern = BLocationManager.CLOSE_MODE;
        pkgs.put(pkg, config);
      }
      return config;
    }
  }

  public int getPattern(int userId, String pkg) {
    synchronized (mLocationConfigs) {
      BLocationConfig config = getOrCreateConfig(userId, pkg);
      return config.pattern;
    }
  }

  @Override
  public void setPattern(int userId, String pkg, int pattern) {
    synchronized (mLocationConfigs) {
      getOrCreateConfig(userId, pkg).pattern = pattern;
      save();
    }
  }

  @Override
  public void setCell(int userId, String pkg, BCell cell) {
    synchronized (mLocationConfigs) {
      getOrCreateConfig(userId, pkg).cell = cell;
      save();
    }
  }

  @Override
  public void setAllCell(int userId, String pkg, List<BCell> cells) {
    synchronized (mLocationConfigs) {
      getOrCreateConfig(userId, pkg).allCell = cells;
      save();
    }
  }

  @Override
  public void setNeighboringCell(int userId, String pkg, List<BCell> cells) {
    synchronized (mLocationConfigs) {
      getOrCreateConfig(userId, pkg).allCell = cells;
      save();
    }
  }

  @Override
  public List<BCell> getNeighboringCell(int userId, String pkg) {
    synchronized (mLocationConfigs) {
      return getOrCreateConfig(userId, pkg).allCell;
    }
  }

  @Override
  public void setGlobalCell(BCell cell) {
    synchronized (mGlobalConfig) {
      mGlobalConfig.cell = cell;
      save();
    }
  }

  @Override
  public void setGlobalAllCell(List<BCell> cells) {
    synchronized (mGlobalConfig) {
      mGlobalConfig.allCell = cells;
      save();
    }
  }

  @Override
  public void setGlobalNeighboringCell(List<BCell> cells) {
    synchronized (mGlobalConfig) {
      mGlobalConfig.neighboringCellInfo = cells;
      save();
    }
  }

  @Override
  public List<BCell> getGlobalNeighboringCell() {
    synchronized (mGlobalConfig) {
      return mGlobalConfig.neighboringCellInfo;
    }
  }

  @Override
  public BCell getCell(int userId, String pkg) {
    BLocationConfig config = getOrCreateConfig(userId, pkg);
    switch (config.pattern) {
      case BLocationManager.OWN_MODE:
        return config.cell;
      case BLocationManager.GLOBAL_MODE:
        return mGlobalConfig.cell;
      case BLocationManager.CLOSE_MODE:
      default:
        return null;
    }
  }

  @Override
  public List<BCell> getAllCell(int userId, String pkg) {
    BLocationConfig config = getOrCreateConfig(userId, pkg);
    switch (config.pattern) {
      case BLocationManager.OWN_MODE:
        return config.allCell;
      case BLocationManager.GLOBAL_MODE:
        return mGlobalConfig.allCell;
      case BLocationManager.CLOSE_MODE:
      default:
        return null;
    }
  }

  @Override
  public void setLocation(int userId, String pkg, BLocation location) {
    synchronized (mLocationConfigs) {
      getOrCreateConfig(userId, pkg).location = location;
      save();
    }
  }

  @Override
  public BLocation getLocation(int userId, String pkg) {
    BLocationConfig config = getOrCreateConfig(userId, pkg);
    switch (config.pattern) {
      case BLocationManager.OWN_MODE:
        return config.location;
      case BLocationManager.GLOBAL_MODE:
        return mGlobalConfig.location;
      case BLocationManager.CLOSE_MODE:
      default:
        return null;
    }
  }

  @Override
  public void setGlobalLocation(BLocation location) {
    synchronized (mGlobalConfig) {
      mGlobalConfig.location = location;
      save();
    }
  }

  @Override
  public BLocation getGlobalLocation() {
    synchronized (mGlobalConfig) {
      return mGlobalConfig.location;
    }
  }

  @Override
  public void requestLocationUpdates(IBinder listener, String packageName, int userId)
      throws RemoteException {
    if (listener == null || !listener.pingBinder()) {
      return;
    }
    if (mLocationListeners.containsKey(listener)) return;
    listener.linkToDeath(
        new DeathRecipient() {
          @Override
          public void binderDied() {
            listener.unlinkToDeath(this, 0);
            mLocationListeners.remove(listener);
          }
        },
        0);
    LocationRecord record = new LocationRecord(packageName, userId);
    mLocationListeners.put(listener, record);
    addTask(listener);
  }

  @Override
  public void removeUpdates(IBinder listener) throws RemoteException {
    if (listener == null || !listener.pingBinder()) {
      return;
    }
    mLocationListeners.remove(listener);
  }

  private void addTask(IBinder locationListener) {
    mThreadPool.execute(
        () -> {
          BLocation lastLocation = null;
          long l = System.currentTimeMillis();
          while (locationListener.pingBinder()) {
            IInterface iInterface = BRILocationListenerStub.get().asInterface(locationListener);
            LocationRecord locationRecord = mLocationListeners.get(locationListener);
            if (locationRecord == null) continue;
            BLocation location = getLocation(locationRecord.userId, locationRecord.packageName);
            if (location == null) continue;
            if (location.equals(lastLocation) && (System.currentTimeMillis() - l) < 3000) {
              try {
                Thread.sleep(1000);
              } catch (InterruptedException ignored) {
              }
              continue;
            }
            lastLocation = location;
            l = System.currentTimeMillis();
            SandBoxCore.get()
                .getHandler()
                .post(
                    () ->
                        BRILocationListener.get(iInterface)
                            .onLocationChanged(location.convert2SystemLocation()));
          }
        });
  }

  public void save() {
    synchronized (mGlobalConfig) {
      synchronized (mLocationConfigs) {
        Parcel parcel = Parcel.obtain();
        AtomicFile atomicFile = new AtomicFile(BEnvironment.getFakeLocationConf());
        FileOutputStream fileOutputStream = null;
        try {
          mGlobalConfig.writeToParcel(parcel, 0);

          parcel.writeInt(mLocationConfigs.size());
          for (int i = 0; i < mLocationConfigs.size(); i++) {
            int tmpUserId = mLocationConfigs.keyAt(i);
            HashMap<String, BLocationConfig> configArrayMap = mLocationConfigs.valueAt(i);
            parcel.writeInt(tmpUserId);
            parcel.writeMap(configArrayMap);
          }
          parcel.setDataPosition(0);
          fileOutputStream = atomicFile.startWrite();
          FileUtils.writeParcelToOutput(parcel, fileOutputStream);
          atomicFile.finishWrite(fileOutputStream);
        } catch (Throwable e) {
          e.printStackTrace();
          atomicFile.failWrite(fileOutputStream);
        } finally {
          parcel.recycle();
          CloseUtils.close(fileOutputStream);
        }
      }
    }
  }

  public void loadConfig() {
    Parcel parcel = Parcel.obtain();
    InputStream is = null;
    try {
      File fakeLocationConf = BEnvironment.getFakeLocationConf();
      if (!fakeLocationConf.exists()) {
        return;
      }
      is = new FileInputStream(BEnvironment.getFakeLocationConf());
      byte[] bytes = FileUtils.toByteArray(is);
      parcel.unmarshall(bytes, 0, bytes.length);
      parcel.setDataPosition(0);

      synchronized (mGlobalConfig) {
        mGlobalConfig.refresh(parcel);
      }

      synchronized (mLocationConfigs) {
        mLocationConfigs.clear();
        int size = parcel.readInt();
        for (int i = 0; i < size; i++) {
          int userId = parcel.readInt();
          HashMap<String, BLocationConfig> configArrayMap =
              parcel.readHashMap(BLocationConfig.class.getClassLoader());
          mLocationConfigs.put(userId, configArrayMap);
          Slog.d(TAG, "load userId: " + userId + ", config: " + configArrayMap);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
      Slog.d(TAG, "bad config");
      FileUtils.deleteDir(BEnvironment.getFakeLocationConf());
    } finally {
      parcel.recycle();
      CloseUtils.close(is);
    }
  }

  @Override
  public void systemReady() {
    loadConfig();
    for (IBinder iBinder : mLocationListeners.keySet()) {
      addTask(iBinder);
    }
  }
}
